package proxy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.TreeMap;

import proxy.repl.RequestCountComparator;
import simulation.Simulator;
import simulation.SimulatorConfig.ScenarioType;
import consumer.Viewing;

/**
 * Search Scenario: A user searches for a tag NOSSDAV
 * 
 * @author Anita Sobe, Wilfried Elmenreich
 * 
 */
public class SearchServerNode extends ServerNode {

	protected TreeMap<Content, Integer> requestCount;
	protected TreeMap<Content, Double> hormoneTable;

	public SearchServerNode(int id, Simulator simulator, Configuration config,
			HormoneControls hCtrl) {
		super(id, simulator, config, hCtrl);
		this.hormoneTable = new TreeMap<Content, Double>();
		this.requestCount = new TreeMap<Content, Integer>();

	}

	public TreeMap<Content, Integer> getRequestCount() {
		return requestCount;
	}

	private void initTagRequestList() {
		for (Content c : simulator.getTagDirectory().keySet()) {
			requestCount.put(c, 0);
		}

	}

	public void countRequest(ArrayList<Viewing> viewings) {
		if (requestCount.size() == 0) {
			initTagRequestList();
		}
		for (Viewing v : viewings) {
			Content current = v.getVideoUnit().getContent();
			int currentCount = requestCount.get(current) + 1;
			requestCount.put(current, currentCount);
		}
	}

	public TreeMap<Content, Double> getHormoneTable() {
		return hormoneTable;
	}

	/**
	 * Increase the hormone of a given tag
	 * 
	 * @param c
	 *            the tag to refer
	 * @param p
	 *            the value of increase
	 */
	public void increaseHormone(Content c, double p) {
		addHormones(c, p);

		if (config.isSearchwithgeneralization()) {
			// find my count in the list
			// 1) calculate how much to give to the general hormone

			ArrayList<Integer> counts = simulator.getTagDirectory().get(c);
			if (counts == null) {
				// if increase of general hormone, then nothing has to be done
				return;
			}

			for (int i = 0; i < simulator.getContentTypes() - 1; i++) {

				Content general = getGeneralToTag(c, i);
				double currentTagValue = hormoneTable.get(c);
				double toput = currentTagValue * hCtrl.getPercentToGeneral()
						* simulator.getSimStep();
				double value = 0;
				if (hormoneTable.containsKey(general)) {

					value = hormoneTable.get(general);

				}
				hormoneTable.put(c, currentTagValue - toput);
				hormoneTable.put(general, value + toput);
			}
		}

	}

	/**
	 * Increase given tag hormone by p
	 * 
	 * @param c
	 * @param p
	 */
	public void addHormones(Content c, double p) {
		double p0 = 0.0;
		if (hormoneTable.containsKey(c))
			p0 = hormoneTable.get(c);
		// value of this unit
		p0 += p;
		// increase by p
		hormoneTable.put(c, p0);
	}

	/**
	 * evaporate pheromone for all entries, remove entries of insignificant
	 * pheromone level possible extension: differentiate evaporation for general
	 * hormones
	 */
	public void evaporateHormones() {

		for (Iterator<Content> iter = hormoneTable.keySet().iterator(); iter
				.hasNext();) {
			Content c = iter.next();
			double p0 = hormoneTable.get(c);
			if (p0 - hCtrl.getEvaporationRate() * simulator.getSimStep() < hCtrl
					.getMinHormone())
				iter.remove(); // avoids ConcurrentModificationException
			else
				hormoneTable.put(
						c,
						p0 - hCtrl.getEvaporationRate()
								* simulator.getSimStep());
		}
	}

	/**
	 * diffuse hormone to neighbors if fitting unit is not located on the node
	 * otherwise stop hormone of tag and of general hormone (partially)
	 */
	public void diffuseHormones() {
		// isolated node, we don't do anything!
		if (links.size() == 0)
			return;

		double sumWeights = 0.0;

		for (Link li : links) {
			double occupiedTime = li.getOccupiedUntil()
					- simulator.getSimTime();
			if (occupiedTime < 0)
				occupiedTime = 0;
			occupiedTime += VideoUnit.avgSize / li.getDatarate();
			sumWeights += 1.0 / occupiedTime;
		}

		for (Link li : links) {

			// calculate link-weight based on link load
			double occupiedTime = li.getOccupiedUntil()
					- simulator.getSimTime();
			if (occupiedTime < 0)
				occupiedTime = 0;
			occupiedTime += VideoUnit.avgSize / li.getDatarate();
			double myWeight = 1.0 / occupiedTime;
			// check if there is anything to be sent
			int preMessageCount = 0;

			for (Content c : hormoneTable.keySet()) {
				// spread to the neighbors
				// this is a heuristic for grading
				Double p0 = hormoneTable.get(c); // links
				Double pheromoneToSpread = p0 * hCtrl.getDiffusionAmount()
						* simulator.getSimStep();
				if (pheromoneToSpread > 0) {
					hormoneTable.put(c, p0 - pheromoneToSpread);

					// stop if unit is there, if specific unit is there also
					// general
					// hormone to it has to be stopped
					if (searchPresentUnits(c).size() > 0) {

						// check general hormones
						if (config.isSearchwithgeneralization()) {
							for (int i = 0; i < simulator.getContentTypes() - 1; i++) {
								Content general = getGeneralToTag(c, i);
								// if there is a general hormone on the node we
								// stop
								// it
								// partially
								if (general != null
										&& hormoneTable.containsKey(general)) {
									double tagValueFromGeneral = (p0 - pheromoneToSpread)
											* hCtrl.getPercentToGeneral()
											* simulator.getSimStep();

									double currentValue = hormoneTable
											.get(general);
									if (currentValue < tagValueFromGeneral) {
										tagValueFromGeneral = currentValue;
									}
									hormoneTable.put(general, currentValue
											- tagValueFromGeneral);

								}
							}
						}

						hormoneTable.put(c, 0.0);
						continue;
					}

					((SearchServerNode) li.getTargetNode()).addHormones(c,
							pheromoneToSpread * myWeight / sumWeights);
					preMessageCount++;

				}
			}
			if (preMessageCount > 0) {
				simulator.messageCount++;
			}
		}
	}

	/**
	 * step 1 (called from simulator over migratevideounits: Goes through all
	 * stored units and checks if there are some worth to move Creates
	 * Migrations for the units to be moved and puts them into the outgoing
	 * folder calls step2: sendSorted
	 */
	protected void putMigrationsToOutgoing() {
		// check all units for transport
		for (Iterator<UnitContainer> it = getStoredUnitIterator(); it.hasNext();) {
			UnitContainer uc = it.next();
			// skip if unit is already being copied
			if (simulator.getSimTime() < uc.copyProcessUntil)
				continue;

			Content current = uc.getVideoUnit().getContent();
			Content[] generals = null;

			if (config.isSearchwithgeneralization()) {
				generals = new Content[simulator.getContentTypes() - 1];
				for (int i = 0; i < generals.length; i++) {
					generals[i] = getGeneralToTag(current, i);
				}
			}
			BestConnection conn = findBestLink(uc, current, generals);
			// didn't find a link or had another problem
			if (conn.bestLink == null)
				continue;

			// we do not have pheromones yet for the current unit!
			if (!hormoneTable.containsKey(current)) {
				hormoneTable.put(current, 0.0);
			}
			double localhormone = 0.0;
			double localGeneralHormone = 0.0;
			localhormone = hormoneTable.get(current);
			if (config.isSearchwithgeneralization()) {
				for (int i = 0; i < simulator.getContentTypes() - 1; i++) {
					if (hormoneTable.containsKey(generals[i])) {
						localGeneralHormone += hormoneTable.get(generals[i]);
					}
				}
			}

			// worth to move/copy?
			if (conn.highestPhero - localhormone > hCtrl
					.getMigrationThreshold()) {
				// check if unit is in use locally
				Migration m = createMigration(uc, conn.highestPhero,
						conn.bestLink);

				if (!getOutgoingUnits().get(links.indexOf(conn.bestLink))
						.contains(m)) {
					getOutgoingUnits().get(links.indexOf(conn.bestLink)).add(m);
					// simulator.unitKeepStat.add((simulator.getSimTime() - uc
					// .getTimeReceived())
					// / (uc.getVideoUnit().getSizeInBytes() / 1000));
				}
			}
			// if there is a strong general hormone on another neighbor - make a
			// replica
			if (config.isSearchwithgeneralization()) {
				if (conn.bestGeneralConnection.bestLink != null
						&& conn.bestLink != conn.bestGeneralConnection.bestLink
						&& conn.bestGeneralConnection.highestPhero
								- localGeneralHormone > hCtrl
									.getMigrationThreshold()) {

					VideoUnit toSend = uc.getVideoUnit();
					// what happens to local unit? should only stay if copied
					// before or if currently in use
					Migration m = createMigration(uc, conn.highestPhero,
							conn.bestLink);

					if (!getOutgoingUnits().get(
							links.indexOf(conn.bestGeneralConnection.bestLink))
							.contains(m)) {
						getOutgoingUnits()
								.get(links
										.indexOf(conn.bestGeneralConnection.bestLink))
								.add(m);
						simulator.movedBecauseOfPrefetching++;
						simulator.log("S" + this.getId() + " puts "
								+ toSend.getContent()
								+ "in outgoing because of general hormone");
					}
				}
			}
		}
		sortOutgoing();
	}

	/**
	 * called by putMigrationsToOutgoing
	 * 
	 * @param uc
	 * @param highestPhero
	 * @param bestlink
	 * @return
	 */
	protected Migration createMigration(UnitContainer uc, double highestPhero,
			Link bestlink) {
		Migration m = null;
		VideoUnit toSend = uc.getVideoUnit();
		boolean isInUse = false;
		if (simulator.getSimTime() < uc.viewedUntil) {
			isInUse = true;
			// create replica
			toSend = (VideoUnit) uc.getVideoUnit().clone();
			toSend.setCreationtime(simulator.getSimTime());
		}
		m = new Migration(toSend, bestlink, isInUse, highestPhero);
		return m;
	}

	/**
	 * Called from PutMigrationsToOutgoing - a unit worth to move is found and
	 * therefore check to which neighbor
	 * 
	 * @param uc
	 * @param current
	 * @param generals
	 * @return
	 */
	private BestConnection findBestLink(UnitContainer uc, Content current,
			Content[] generals) {
		BestConnection conn = new BestConnection();
		BestConnection genConn = null;
		if (generals != null) {
			genConn = new BestConnection();
			conn.bestGeneralConnection = genConn;
		}
		// go through all links and find best neighbor
		for (Link li : links) {
			SearchServerNode target = (SearchServerNode) li.getTargetNode();
			// get another link if this one is not usable
			if ((li.getOccupiedUntil() - simulator.getSimTime() > hCtrl
					.getBusyLinkThreshold())
					|| (target.diskFreeinBytes() < uc.getVideoUnit()
							.getSizeInBytes()))
				continue;

			if (simulator.getScenario() == ScenarioType.SEARCH
					|| simulator.getScenario() == ScenarioType.UNITSIZE) {

				if (target.isOneInIncomingUnits(uc.getVideoUnit().getContent())
						|| target.searchPresentUnits(
								uc.getVideoUnit().getContent()).size() > 0)
					continue;
			} else {
				if (target.searchInIncomingUnits(uc.getVideoUnit().getId()) != null
						|| target.searchPresentUnit(uc.getVideoUnit().getId()) != null)
					continue;

			}

			if (target.hormoneTable.containsKey(current)) {
				Double p = target.hormoneTable.get(current);
				if (p > conn.highestPhero) {
					conn.highestPhero = p;
					conn.bestLink = li;
				}
			}
			if (generals != null && config.isSearchwithgeneralization()) {
				// check if neighbor has general hormone alone if it is
				// strong enough we can move the unit
				SearchServerNode neighbor = (SearchServerNode) li
						.getTargetNode();
				double p = 0;

				// add partially the general pheromones of the targetNode
				for (int i = 0; i < simulator.getContentTypes() - 1; i++) {
					if (neighbor.hormoneTable.containsKey(generals[i])
							&& hormoneTable.containsKey(generals[i])) {
						p += neighbor.hormoneTable.get(generals[i]);
					}
				}
				if (p > conn.highestPhero) {
					genConn.highestPhero = p;
					genConn.bestLink = li;
				}
			}
		}
		return conn;
	}

	/**
	 * Start to increase hormones regularly depending on the current requests
	 * 
	 * @param currentlyDemanding
	 * @param newRequest
	 */
	public void increaseInterest(List<Viewing> currentlyDemanding,
			boolean newRequest, boolean sequential) {

		if (currentlyDemanding.size() == 0)
			return;

		// hormone model
		double hormone;
		// handle different hormone amounts
		if (newRequest) {
			hormone = hCtrl.getHAmount1() * simulator.getSimStep();
		} else {
			hormone = hCtrl.getHAmount2() * simulator.getSimStep();
		}

		for (Viewing vi : currentlyDemanding) {
			if (sequential) {
				hormone *= (currentlyDemanding.size() - currentlyDemanding
						.indexOf(vi));
			}
			increaseHormone(vi.getVideoUnit().getContent(), hormone);
		}
	}

	public void decreaseInterest(Viewing vi) {
	}

	@Override
	public boolean performThreeWayCleanupForUnit(int id) {
		UnitContainer uc = searchPresentUnit(id);
		if (uc == null)
			return false;
		// try to delete if a another copy is available in the system
		if (super.countUnitOnNeighbors(uc.getVideoUnit().getId()) > 0) {
			// remove this unit
			simulator.countRemoved++;
			removeUnitFromStorage(uc);
			simulator.log("S" + id + ": removed u" + uc.getVideoUnit().getId());
			logUnitList();
			return true; // was immediately deleted
		}

		// the unit cannot be deleted, but perhaps we can move it to another
		// server

		// select possible target node with respective pheromone
		BestConnection conn = findBestLink(uc, uc.getVideoUnit().getContent(),
				null);
		if (conn.bestLink != null) {
			Migration m = new Migration(uc.getVideoUnit(), conn.bestLink,
					false, conn.highestPhero);
			if (!getOutgoingUnits().get(links.indexOf(conn.bestLink)).contains(
					m)) {

				getOutgoingUnits().get(links.indexOf(conn.bestLink)).add(m);
				Collections.sort(getOutgoingUnits().get(
						links.indexOf(conn.bestLink)));
				// send one to get rid of any unit
				Migration mC = getOutgoingUnits().get(
						links.indexOf(conn.bestLink)).get(0);
				if (migrateUnitTo(mC.getVideoUnit(), mC.getLink(), mC.isCopy())) {
					// simulator.countMovedPhero++;
					simulator.log("S" + this.getId()
							+ ": cleanup moving(phero) u"
							+ mC.getVideoUnit().getId() + " to S"
							+ conn.bestLink.getTargetNode().getId() + ":");
				}
				return false;
			}
		}
		// did not work, try to push this unit to a server with more free space
		conn.bestLink = null;
		int myFreeSpace = diskFreeinBytes();
		int largestFreeSpace = myFreeSpace + uc.getVideoUnit().getSizeInBytes();
		for (Link li : links) {
			if ((li.getOccupiedUntil() - simulator.getSimTime() > hCtrl
					.getBusyLinkThreshold())
					|| (li.getTargetNode().diskFreeinBytes() < uc
							.getVideoUnit().getSizeInBytes()))
				continue;
			if (li.getTargetNode().searchInIncomingUnits(
					uc.getVideoUnit().getId()) != null
					|| li.getTargetNode().searchPresentUnit(
							uc.getVideoUnit().getId()) != null)
				continue;
			int targetFreeSpace = li.getTargetNode().diskFreeinBytes();
			if (targetFreeSpace > largestFreeSpace) {
				largestFreeSpace = targetFreeSpace;
				conn.bestLink = li;
			}
		}
		if (conn.bestLink != null) {

			double importance = 0d;
			if (((SearchServerNode) conn.bestLink.getTargetNode()).hormoneTable
					.containsKey(uc.getVideoUnit().getContent())) {
				importance = ((SearchServerNode) conn.bestLink.getTargetNode()).hormoneTable
						.get(uc.getVideoUnit().getContent());
			}

			Migration m1 = new Migration(uc.getVideoUnit(), conn.bestLink,
					false, importance);

			if (!getOutgoingUnits().get(links.indexOf(conn.bestLink)).contains(
					m1)) {
				getOutgoingUnits().get(links.indexOf(conn.bestLink)).add(m1);
				Collections.sort(getOutgoingUnits().get(
						links.indexOf(conn.bestLink)));
				Migration mC = getOutgoingUnits().get(
						links.indexOf(conn.bestLink)).get(0);
				if (migrateUnitTo(mC.getVideoUnit(), mC.getLink(), mC.isCopy())) {
					// simulator.countMovedSpace++;
					simulator.log("S" + this.getId()
							+ ": cleanup moving(diskcap) u"
							+ mC.getVideoUnit().getId() + " to S"
							+ conn.bestLink.getTargetNode().getId() + ":");
				}
				return false;
			}
		}
		simulator.countCleanupFailed++;
		return false;
	}

	public Content getGeneralToTag(Content c, int dist) {
		Content general = new Content(simulator.getContentTypes());
		for (int j = 0; j < simulator.getContentTypes() - 1; j++) {
			general.content[j] = c.content[j];
			if (j == dist) {
				return general;
			}
		}
		return null;

	}

	@Override
	public void addUnitToStorage(UnitContainer uc) {
		super.addUnitToStorage(uc);
		if (config.isSearchwithgeneralization()) {

			Content gen = getGeneralToTag(uc.getVideoUnit().getContent(),
					simulator.getContentTypes() - 2);
			if (gen != null) {

				super.addContentToStorage(gen, uc);
			}

		}
	}

	@Override
	public void removeUnitFromStorage(UnitContainer uc) {
		super.removeUnitFromStorage(uc);
		if (config.isSearchwithgeneralization()) {

			Content gen = getGeneralToTag(uc.getVideoUnit().getContent(),
					simulator.getContentTypes() - 2);
			if (gen != null) {
				super.removeContentFromStorage(gen, uc);
			}

		}
	}

	@Override
	public ArrayList<UnitContainer> searchPresentUnits(Content c) {
		ArrayList<UnitContainer> result = super.searchPresentUnits(c);

		if (config.isSearchwithgeneralization() && result.size() == 0) {

			Content gen = getGeneralToTag(c, simulator.getContentTypes() - 2);
			if (gen != null) {
				ArrayList<UnitContainer> res = super.searchPresentUnits(gen);
				if (res.size() > 0) {
					result.add(res.get(0));
				}
			}
		}
		return result;
	}

	@Override
	boolean performLFUCleanup() {

		Comparator<Content> comp = new RequestCountComparator(requestCount);
		ArrayList<Content> mostPopular = new ArrayList<Content>();
		mostPopular.addAll(requestCount.keySet());
		Collections.sort(mostPopular, comp);
		Collections.reverse(mostPopular);
		// find least frequently requested unit in storage

		ArrayList<UnitContainer> foundUnits = new ArrayList<UnitContainer>();
		for (int i = 0; i < mostPopular.size(); i++) {
			if ((foundUnits = searchPresentUnits(mostPopular.get(i))).size() > 0) {
				break;
			}
		}
		if (foundUnits.size() == 0) {
			simulator.countCleanupFailed++;
			return false;
		}

		if (countUnitOnNeighbors(foundUnits.get(0).getVideoUnit().getContent()) > 0) {
			for (int i = 0; i < foundUnits.size(); i++) {
				double lastTimeUsed = simulator.getSimTime()
						- foundUnits.get(i).lastUse();
				if (lastTimeUsed > hCtrl.getCleanUpTriggerTime()) {
					removeUnitFromStorage(foundUnits.get(i));
					simulator.countRemoved++;
					simulator.log("S" + getId() + " [LFU] removed unit "
							+ foundUnits.get(i).getVideoUnit()
							+ " popularity: " + (requestCount.size() - i) + "/"
							+ requestCount.size());
					return true;
				}
			}
		}
		return false;

	}

	boolean performHormoneCleanup() {
		// find those units that don't have any hormones on neighbors
		// 1) go through stored units
		// 2) collect those which don't have hormones on neighbors
		ArrayList<UnitContainer> deleteCandidates = new ArrayList<UnitContainer>();
		for (Iterator<UnitContainer> it = getStoredUnitIterator(); it.hasNext();) {
			UnitContainer current = it.next();
			int count = links.size();
			if (countUnitOnNeighbors(current.getVideoUnit().getContent()) > 0) {
				for (Link li : links) {
					SearchServerNode n = (SearchServerNode) li.getTargetNode();
					if (this.hormoneTable.containsKey(current.getVideoUnit()
							.getContent())
							|| n.hormoneTable.containsKey(current
									.getVideoUnit().getContent())) {
						break;
					} else {
						count--;
					}
				}
				if (count == 0) {
					deleteCandidates.add(current);
				}
			}
		}
		if (deleteCandidates.size() > 0) {
			for (UnitContainer uc : deleteCandidates) {
				if (simulator.getSimTime() - uc.lastUse() > hCtrl
						.getCleanUpTriggerTime()) {
					removeUnitFromStorage(uc);
					simulator.countRemoved++;
					return true;
				}
			}
		} else {
			simulator.countCleanupFailed++;

		}
		return false;
	}

}
